<?php
  // this is attempt to cleanup websockets server demo
  // NOTE: Do not load this page in web browser because browsers cache displaying! Use wget -qO - http://localhost/tmweb/websockets/server.php

  error_reporting(E_ALL);
  set_time_limit(0);
  ob_implicit_flush();

  function debug($AMsg="") { 
    // echo message and flush output
    echo "$AMsg\n";
    flush();
    ob_flush();
  }

  // main loop
  $master  = WebSocket("localhost",12500);
  $sockets = array($master);
  $users   = array();
  while(true){
    $changed = $sockets;
    socket_select($changed,$write=NULL,$except=NULL,NULL);
    foreach($changed as $socket){
      if($socket==$master){
        $client=socket_accept($master);
        if($client<0) { debug("socket_accept() failed"); continue; }
        else{ connect($client); }
      }
      else{
        $bytes = @socket_recv($socket,$buffer,2048,0);
        if($bytes==0){ disconnect($socket); }
        else{
          $user = getuserbysocket($socket);
          if(!$user->handshake){ dohandshake($user,$buffer); }
          else{ process($user,$buffer); }
        }
      }
    }
  }

  function process($user,$msg){
    $action = unwrap($msg);
    say("< ".$action);
    switch($action){
      case "hello" : send($user->socket,"hello human");                       break;
      case "hi"    : send($user->socket,"zup human");                         break;
      case "name"  : send($user->socket,"my name is Multivac, silly I know"); break;
      case "age"   : send($user->socket,"I am older than time itself");       break;
      case "date"  : send($user->socket,"today is ".date("Y.m.d"));           break;
      case "time"  : send($user->socket,"server time is ".date("H:i:s"));     break;
      case "thanks": send($user->socket,"you're welcome");                    break;
      case "bye"   : send($user->socket,"bye");                               break;
      default      : send($user->socket,$action." not understood (this is reply from websocket server!)");           break;
    }
  }

  function send($client,$msg){
    say("> ".$msg);
    $msg = wrap($msg);
    socket_write($client,$msg,strlen($msg));
  }
  
  function WebSocket($address,$port){
    echo "Trying create/reuse/bind/listen WebSocket : ".$address." port ".$port."\n\r\n\r";
    $master=socket_create(AF_INET, SOCK_STREAM, SOL_TCP)     or die("socket_create() failed");
    socket_set_option($master, SOL_SOCKET, SO_REUSEADDR, 1)  or die("socket_option() failed");
    socket_bind($master, $address, $port)                    or die("socket_bind() failed");
    socket_listen($master,20)                                or die("socket_listen() failed");
    echo "Server Started : ".date('Y-m-d H:i:s')."\n";
    echo "Master socket  : ".$master."\n";
    echo "Listening on   : ".$address." port ".$port."\n\n";
    return $master;
  }

  function connect($socket){
    global $sockets,$users;
    $user = new User();
    $user->id = uniqid();
    $user->socket = $socket;
    array_push($users,$user);
    array_push($sockets,$socket);
    debug($socket." CONNECTED!");
  }
  
  function disconnect($socket){
    global $sockets,$users;
    $found=null;
    $n=count($users);
    for($i=0;$i<$n;$i++){
      if($users[$i]->socket==$socket){ $found=$i; break; }
    }
    if(!is_null($found)){ array_splice($users,$found,1); }
    $index = array_search($socket,$sockets);
    socket_close($socket);
    debug($socket." DISCONNECTED!");
    if($index>=0){ array_splice($sockets,$index,1); }
  }

  function dohandshake($user,$buffer){
    debug("\nRequesting handshake...");
    debug($buffer);
    list($resource,$host,$origin,$strkey1,$strkey2,$data) = getheaders($buffer);
    debug("Handshaking...");
  
    $pattern = '/[^\d]*/';
    $replacement = '';
    $numkey1 = preg_replace($pattern, $replacement, $strkey1);
    $numkey2 = preg_replace($pattern, $replacement, $strkey2);
  
    $pattern = '/[^ ]*/';
    $replacement = '';
    $spaces1 = strlen(preg_replace($pattern, $replacement, $strkey1));
    $spaces2 = strlen(preg_replace($pattern, $replacement, $strkey2));
  
    if ($spaces1 == 0 || $spaces2 == 0 || $numkey1 % $spaces1 != 0 || $numkey2 % $spaces2 != 0) {
    socket_close($user->socket);
    debug('failed');
    return false;
    }
  
    $ctx = hash_init('md5');
    hash_update($ctx, pack("N", $numkey1/$spaces1));
    hash_update($ctx, pack("N", $numkey2/$spaces2));
    hash_update($ctx, $data);
    $hash_data = hash_final($ctx,true);
  
    $upgrade  = "HTTP/1.1 101 WebSocket Protocol Handshake\r\n" .
                "Upgrade: WebSocket\r\n" .
                "Connection: Upgrade\r\n" .
                "Sec-WebSocket-Origin: " . $origin . "\r\n" .
                "Sec-WebSocket-Location: ws://" . $host . $resource . "\r\n" .
                "\r\n" .
                $hash_data;
  
    socket_write($user->socket,$upgrade.chr(0),strlen($upgrade.chr(0)));
    $user->handshake=true;
    debug($upgrade);
    debug("Done handshaking...");
    return true;
  }

  function getheaders($req){
    $r=$h=$o=null;
    if(preg_match("/GET (.*) HTTP/"   ,$req,$match)){ $r=$match[1]; }
    if(preg_match("/Host: (.*)\r\n/"  ,$req,$match)){ $h=$match[1]; }
    if(preg_match("/Origin: (.*)\r\n/",$req,$match)){ $o=$match[1]; }
    if(preg_match("/Sec-WebSocket-Key2: (.*)\r\n/",$req,$match)){ $key2=$match[1]; }
    if(preg_match("/Sec-WebSocket-Key1: (.*)\r\n/",$req,$match)){ $key1=$match[1]; }
    if(preg_match("/\r\n(.*?)\$/",$req,$match)){ $data=$match[1]; }
    return array($r,$h,$o,$key1,$key2,$data);
  }

  function getuserbysocket($socket){
    global $users;
    $found=null;
    foreach($users as $user){
      if($user->socket==$socket){ $found=$user; break; }
    }
    return $found;
  }

  function say($msg="") {
    echo $msg."\n"; 
  }
  
  function wrap($msg="") {
    return chr(0).$msg.chr(255);
  }
  
  function unwrap($msg="") {
    return substr($msg,1,strlen($msg)-2);
  }

  class User{
    var $id;
    var $socket;
    var $handshake;
  }

?>
